Stream Sensor Base Station

12/18/2020
Jake Robinson
jnr62


Project Objectives:

  • Design a base station that can receive stream sensor data transmitted over LoRa radio.
  • Store the data in an onboard database for easy retrieval.
  • Design a touchscreen user interface that allows the user to view data stored in the database.
  • Test the maximum range of the LoRa radio receiver

Introduction

This project is part of a larger MEng project to design a network of floating sensors to collect data on plastic pollution in streams. All that data will be transmitted and collected in this base station for easy retrieval. For this purpose, the base station consists of a LoRa radio receiver, to receive wireless data transmissions from the sensors, connected to a Raspberry Pi 4, which handles data storage and retrieval. For the purposes of testing, I used a dummy sensor with a LoRa transmitter, which transmits sample sensor datasets. A database on the Raspberry Pi stores and organizes the received data, and a piTFT touchscreen displays a user interface which makes it easy to navigate the data stored on the the database.

System Diagram


Design and Testing

The first step in designing the base station was adding a LoRa receiver. I used an Adafruit LoRa breakout board, which interfaces with the Raspberry Pi using SPI. Adafruit provides a CircuitPython library for the LoRa radio. CicuitPython is a simplified version of Python for microcontrollers, so I had to download CircuitPython on the RaspberryPi. I ran into some trouble when I was trying to enable SPI. Becasue the piTFT usew SPI0, I could not enable that SPI port to use for the LoRa board. I had to enable SPI1 by modifying /boot/config.txt to include the line dtoverlay=spi1-1cs. Then, I was able to connect the LoRa board to the SPI1 MISO MOSI, and SCLK pins: GPIO 19, 20, and 21 respectively. With the hardware setup, I wrote a python program with the adafruit_rfm9x library called receiver.py. This program fetches packets received by the LoRa board and parses the packet to extract individual data values. After I set up the database on the Raspberry Pi, I modified receiver.py to include os.system calls to store the parsed data values in the database.

LoRa Breakout Board

To test the receiver, I used a Feather M0 with a built in LoRa transmitter to send data packets. I used the arduino RH_RF95 library, and modified the example transmitter code to create a program called dummy_node, which transmits a series of 10 latitude, longitude, and velocity values, with date and timestamps, which I randomly generated for the sake of testing.

Adafruit Feather M0

The database I used was MariaDB. Originally, I planned to use a MySQL database, but I found out that MySQL is no longer up to date, so I used MariaDB instead. I had to spend time reading through MariaDB tutorials to figure out how the database interface works. I downloaded MariaDB on the Raspberry Pi, and created a database called "basestation", which has two tables of data. The first table, "sensors", contains one column with a list of all the sensors that the bases station has recieved data from. The second table, "sensor_data", contains columns for sensor IDs, date and time, latitude, longitude, and velocity values for each data set received. The MariaDB interface makes it easy to select data from a specific sensor.

Tables in MariaDB Database

The final part of the system was the user interface. I wrote another python scrpit on the Raspberry Pi called user_interface.py, which uses Pygame to draw the UI on the piTFT screen. The user interface code also uses the mysql library to fetch data from the database. The startup screen of the UI lists the sensor IDs of all data in the database, fetched from the "sensors" table. Up to seven sensor IDs can be displayed on the screen at once, and arrow buttons on the left and right of the screen can be used to scroll through and see more. The startup screen also includes a "refresh" button, to update the screen when new data has been received. When the user taps on one of the sensor IDs, the UI switches to the next screen which lists the date and time stamps of all the data received from that sensor. Once again, only seven values can be displayed at once, and the user can scroll through with arrow buttons. When he user taps on one of the timestamps, the UI displays a third screen with the longitude, latitude, and velocity values taken at that time.

First Screen of User Interface

Second Screen of User Interface

Third Screen of User Interface

To test the range of the LoRa base station, I placed the feather M0 outsidemy house, and started transmitting data. Then, I drove around nearby roads with the base station to find the furthest spots where it could still receive data. I marked all these apots on a map, then measured the distances to find the average range.


Results

Although it took some time to debug the different modules used in this project, I got everything to perform as expected. The system I built was able to receive data transmitted by a sensor, store it in a database and display data on the piTFT screen.

The maximum range of the LoRa radio vaired from around 165 ft. to 322 ft., with an average maximum of 243 ft. It seems that having to transmit through buildings or other obstacles significanly decreases range.

Range Measurements


Conclusions

I successfully built a base station receiver that works as intended. The station is able to store data from the LoRa receiver in a MariaDB database. The piTFT user interface makes it easy to navigate through the data on the database. The biggest issue with the the station as it exists currently is the LoRa range. To be useful in the field, the communication range should be greater than a few hundred feet.


Future Work

In the future, I would try to increase the range of the LoRa communications by placing the transmitter and receiver up high, to transmit around obstacles and cover a larger range. I would also work on improving the user interface to make it easier to analyze the data. I could connect the base station to a web app that lets the user view a map of where the sensors have been. Finally, I would test the base station with multiple sensors. I would have to develop a communication protocol to avoid wireless packet collision.


Parts List

Total: $90.9


References

LoRa Radio Breakout
CircuitPython on Raspberry Pi
RFM95 CircuitPython Library
Adafruit Feather M0
MariaDB Tutorial

Code Appendix


# receiver.py
import digitalio
import board
import busio
import adafruit_rfm9x
import os

RADIO_FREQ_MHZ = 915.0
CS = digitalio.DigitalInOut(board.D6)
RESET = digitalio.DigitalInOut(board.D13)
spi = busio.SPI(board.D21, MOSI=board.D20, MISO=board.D19)
rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)

while True:
    packet = rfm9x.receive()  # Wait for a packet to be received (up to 0.5 seconds)
    if packet is not None:
        packet_text = str(packet, 'ascii')
        print('Received: {0}'.format(packet_text))
        packet_text_array = packet_text.split(',')

        sensor_id = packet_text_array[0]
        hour = packet_text_array[1]
        minute = packet_text_array[2]
        second = packet_text_array[3]
        day = packet_text_array[4]
        month = packet_text_array[5]
        year = packet_text_array[6]
        datetime = year+"-"+month+"-"+day+" "+hour+":"+minute+":"+second
        latitude = packet_text_array[7]
        longitude = packet_text_array[8]
        velocity = packet_text_array[9]

        os.system("sudo mysql -u root -pstreamsensor -D basestation -e \"insert into sensors(sensor_id) values("+sensor_id+");\"")
        os.system("sudo mysql -u root -pstreamsensor -D basestation -e \"insert into sensor_data(sensor_id, date_time, lat, lon, vel) values(\'"+sensor_id+"\',\'"+datetime+"\',\'"+latitude+"\',\'"+longitude+"\',\'"+velocity+"\');\"")

              

# user_interface.py
import pygame
from pygame.locals import*
import os
import time
import mysql.connector


def update():
    global layer
    global page
    global mycursor
    global sensor
    global date_time
    global myresult
    screen.fill(BLACK)
    if (layer == 1):
        my_buttons = {'<':(20,120), '>':(300,120), 'REFRESH':(270,220), 'Sensors':(160,20)}
        for my_text, text_pos in my_buttons.items():
            text_surface = my_font.render(my_text, True, WHITE)
            rect = text_surface.get_rect(center=text_pos)
            screen.blit(text_surface, rect)
        mycursor.execute("SELECT sensor_id FROM sensors")
        myresult = mycursor.fetchall()
        for i in range((page-1)*7,len(myresult)):
            text_surface = my_font.render(str(myresult[i][0]), True, WHITE)
            rect = text_surface.get_rect(center=(160, 50+30*(i-7*(page-1))))
            screen.blit(text_surface, rect)
    elif (layer == 2):
        my_buttons = {'<':(20,120), '>':(300,120), 'BACK':(290,220), 'Date':(130,20), 'Time':(200,20)}
        for my_text, text_pos in my_buttons.items():
            text_surface = my_font.render(my_text, True, WHITE)
            rect = text_surface.get_rect(center=text_pos)
            screen.blit(text_surface, rect)
        mycursor.execute("SELECT date_time FROM sensor_data WHERE sensor_id = "+str(sensor)+" order by date_time")
        myresult = mycursor.fetchall()
        for i in range((page-1)*7,len(myresult)):
            text_surface = my_font.render(str(myresult[i][0]), True, WHITE)
            rect = text_surface.get_rect(center=(160, 50+30*(i-7*(page-1))))
            screen.blit(text_surface, rect)
    elif (layer == 3):
        my_buttons = {'Latitude':(160,20), 'Longitude':(160,100), 'Velocity':(160,180), 'BACK':(290,220)}
        for my_text, text_pos in my_buttons.items():
            text_surface = my_font.render(my_text, True, WHITE)
            rect = text_surface.get_rect(center=text_pos)
            screen.blit(text_surface, rect)
        mycursor.execute("SELECT lat, lon, vel FROM sensor_data WHERE sensor_id = "+str(sensor)+" and date_time = \'"+date_time+"\'")
        myresult = mycursor.fetchall()
        text_surface = my_font.render(str(myresult[0][0]), True, WHITE)
        rect = text_surface.get_rect(center=(160,50))
        screen.blit(text_surface, rect)
        text_surface = my_font.render(str(myresult[0][1]), True, WHITE)
        rect = text_surface.get_rect(center=(160,130))
        screen.blit(text_surface, rect)
        text_surface = my_font.render(str(myresult[0][2]), True, WHITE)
        rect = text_surface.get_rect(center=(160,210))
        screen.blit(text_surface, rect)
    pygame.display.flip()

os.putenv('SDL_VIDEODRIVER', 'fbcon')
os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

pygame.init()
pygame.mouse.set_visible(False)
WHITE = 255, 255, 255
BLACK = 0, 0, 0
screen = pygame.display.set_mode((320, 240))
my_font = pygame.font.Font(None, 25)

mydb = mysql.connector.connect(
  host="localhost",
  user="root",
  password="streamsensor",
  database="basestation",
)
mycursor = mydb.cursor()

layer = 1
page = 1
sensor = 1
date_time = "2020-12-12 12:00:00"
myresult = []

update()
while True:
    time.sleep(0.2)
    for event in pygame.event.get():
        if(event.type is MOUSEBUTTONUP):
            pos = pygame.mouse.get_pos()
            x,y = pos
            if layer == 1:
                if x>220:
                    if y>200:
                        mydb = mysql.connector.connect(
                          host="localhost",
                          user="root",
                          password="streamsensor",
                          database="basestation",
                        )
                        mycursor = mydb.cursor()
                        update()
                    else:
                        page = page+1
                        update()
                elif x<100:
                    if page>1:
                        page = page-1
                        update()
                else:
                    if y>35:
                        if y>65:
                            if y>95:
                                if y>125:
                                    if y>155:
                                        if y>185:
                                            if y>215:
                                                n = 7*(page-1)+7
                                            else:
                                                n = 7*(page-1)+6
                                        else:
                                            n = 7*(page-1)+5
                                    else:
                                        n = 7*(page-1)+4
                                else:
                                    n = 7*(page-1)+3
                            else:
                                n = 7*(page-1)+2
                        else:
                            n = 7*(page-1)+1
                        if n<=len(myresult):
                            sensor = myresult[n-1][0]
                            layer = 2
                            page = 1
                            update()
            elif layer == 2:
                if x>260:
                    if y>200:
                        layer = 1
                        page = 1
                        update()
                    else:
                        page = page+1
                        update()
                elif x>60:
                    if page>1:
                        page = page-1
                        update()
                else:
                    if y>35:
                        if y>65:
                            if y>95:
                                if y>125:
                                    if y>155:
                                        if y>185:
                                            if y>215:
                                                n = 7*(page-1)+7
                                            else:
                                                n = 7*(page-1)+6
                                        else:
                                            n = 7*(page-1)+5
                                    else:
                                        n = 7*(page-1)+4
                                else:
                                    n = 7*(page-1)+3
                            else:
                                n = 7*(page-1)+2
                        else:
                            n = 7*(page-1)+1
                        if n<=len(myresult):
                            date_time = str(myresult[n-1][0])
                            layer = 3
                            page = 1
                            update()
            elif layer == 3:
                if x>260:
                    if y>200:
                        layer = 2
                        page = 1
                        update()

              

// dummy_node
used the #include <SPI.h>
#include <RH_RF95.h>

// for feather m0  
#define RFM95_CS 8
#define RFM95_RST 4
#define RFM95_INT 3

// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 915.0

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

int node_id = 1;
int time_dat[10][6] = {{12, 0, 0, 12, 12, 2020}, {12, 0, 45, 12, 12, 2020}, {12, 1, 30, 12, 12, 2020}, {12, 2, 15, 12, 12, 2020}, {12, 3, 0, 12, 12, 2020}, {12, 3, 45, 12, 12, 2020}, {12, 4, 30, 12, 12, 2020}, {12, 5, 15, 12, 12, 2020}, {12, 6, 0, 12, 12, 2020}, {12, 6, 45, 12, 12, 2020}};
float gps_dat[10][3] = {{40.6091, -74.5580, 0.14}, {40.6893, -74.5436, 0.15}, {40.6344, -74.5436, 0.16}, {40.6945, -74.5438, 0.17}, {40.6164, -74.5434, 0.18}, {40.6156, -74.5465, 0.19}, {40.6342, -74.5483, 0.2}, {40.6438, -74.5438, 0.21}, {40.6541, -74.5543, 0.22}, {40.6784, -74.5174, 0.23}};

int i = 0;
int data_len = 10;

void setup() 
{
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, HIGH);
 /*
  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }
 */
  delay(100);
 
  //Serial.println("Feather LoRa TX Test!");
 
  // manual reset
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);
 
  while (!rf95.init()) {
    //Serial.println("LoRa radio init failed");
    //Serial.println("Uncomment '#define SERIAL_DEBUG' in RH_RF95.cpp for detailed debug info");
    while (1);
  }
  //Serial.println("LoRa radio init OK!");
 
  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf95.setFrequency(RF95_FREQ)) {
    //Serial.println("setFrequency failed");
    while (1);
  }
  //Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);
  
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on
 
  // The default transmitter power is 13dBm, using PA_BOOST.
  // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then 
  // you can set transmitter powers from 5 to 23 dBm:
  rf95.setTxPower(23, false);
}
 
int16_t packetnum = 0;  // packet counter, we increment per xmission

void loop()
{
  if (i<data_len) {
    char radiopacket[50];
    sprintf(radiopacket, "%d,%.2d,%.2d,%.2d,%.2d,%.2d,%.4d", node_id, time_dat[i][0], time_dat[i][1], time_dat[i][2], time_dat[i][3], time_dat[i][4], time_dat[i][5]);
    String packet(radiopacket);
    
    String Lat = String(gps_dat[i][0], 4);
    String Lon = String(gps_dat[i][1], 4);
    String Vel = String(gps_dat[i][2], 4);
    (packet+","+Lat+","+Lon+","+Vel+",").toCharArray(radiopacket, 50);
    
    //Serial.println(radiopacket);
    
    rf95.send((uint8_t*)radiopacket, sizeof(radiopacket));
    i++;
    delay(1000); // Wait 1 second between transmits, could also 'sleep' here!
  }
}